package dk.au.cs.peterbn.processing.compilationJobs;

import dk.au.cs.peterbn.annotations.Member;
import dk.au.cs.peterbn.patterns.MemberRole;
import dk.au.cs.peterbn.patterns.TypeRole;
import dk.au.cs.peterbn.processing.PatternInstance;
import dk.au.cs.peterbn.processing.ProcessingJob;
import dk.au.cs.peterbn.processing.ProcessingUtil;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.util.*;

import static javax.lang.model.util.ElementFilter.*;
import static javax.lang.model.util.ElementFilter.typesIn;

/**
 * Check that each type has the members required by it's role(s)
 * <p/>
 * A method is taken into consideration if it is annotated with either @Member or @Override.
 */
public class CheckAllMemberRolesPresent implements ProcessingJob {

    private final Map<String, PatternInstance> patternInstances;
    private final ProcessingUtil util;

    public CheckAllMemberRolesPresent(Map<String, PatternInstance> patternInstances, ProcessingUtil util) {
        this.patternInstances = patternInstances;
        this.util = util;
    }

    @Override
    public void process() {
        Set<Element> elements = new HashSet<Element>();
        for (PatternInstance patternInstance : patternInstances.values()) {
            elements.addAll(patternInstance.getElements());
        }
        Set<TypeElement> typeElements = typesIn(elements);
        //Now, we want to check all the type elements
        for (TypeElement element : typeElements) {
            processTypeElement(element);
        }
    }

    /**
     * Process a single type element
     *
     * @param element The element to process.
     */
    private void processTypeElement(TypeElement element) {
        List<AnnotationMirror> patternAnnotations = util.getPatternAnnotationMirrors(element);
        //We need all members in case we are inheriting some of the methods
        //Check each pattern the type participates in
        for (AnnotationMirror patternAnnotation : patternAnnotations) {
            String annotationId = util.getId(patternAnnotation);
            TypeRole typeRole = util.getTypeRole(element, patternAnnotation);
            if (typeRole == null) {
                continue; //An error has already been filed. Just get on with it.
            }

            Set<MemberRole> formalMemberRoles = new HashSet<MemberRole>(typeRole.getMembers().values());

            //Check the whole inheritance tree until we have found all our wayward members.
            //This way of doing things stands instead of using @Override, which is Retention SOURCE... >:-(
            //On the plus side, the code for doing it is short.
            Queue<TypeElement> typeTree = new LinkedList<TypeElement>();
            typeTree.add(element);
            TypeElement currentelement;
            while (!formalMemberRoles.isEmpty() && (currentelement = typeTree.poll()) != null) {
                List<? extends TypeMirror> interfaces = currentelement.getInterfaces();
                for (TypeMirror anInterface : interfaces) {
                    //Yes, the same interfaces might be added a few times here, however it should only add to the loop time, as the
                    //loop operation is idempotent for the same TypeElement
                    TypeElement e = util.asTypeElement(anInterface);
                    if (e != null) {
                        //because adding null to a queue where the exit-condition is getting null is a Bad Idea (tm)
                        typeTree.add(e);
                    }
                }
                if (currentelement.getSuperclass() != null) {
                    TypeElement e = util.asTypeElement(currentelement.getSuperclass());
                    if (e != null) {
                        typeTree.add(e);
                    }
                }

                List<ExecutableElement> containedMembers = methodsIn(currentelement.getEnclosedElements());
                for (ExecutableElement typeMember : containedMembers) {
                    //Check each method
                    AnnotationMirror mirror = util.getMemberAnnotation(typeMember, annotationId);
                    if (mirror != null) {
                        String memberPatternId = util.getId(mirror);
                        if (memberPatternId.equals(annotationId)) {
                            String roleName = util.getValue(mirror, Member.Fields.ROLE);
                            MemberRole memberRole = patternInstances.get(memberPatternId).getName().getMember(roleName);
                            if (memberRole != null) {
                                formalMemberRoles.remove(memberRole);
                            }
                        }
                    }
                }
            }

            if (!formalMemberRoles.isEmpty()) {
                for (MemberRole formalMemberRole : formalMemberRoles) {
                    if (formalMemberRole.isRequired()) {
                        util.printMessage(Diagnostic.Kind.ERROR,
                                "Missing required pattern member: " +
                                        formalMemberRole.getName(), element, patternAnnotation);
                    } else {
                        util.printMessage(Diagnostic.Kind.WARNING,
                                "Missing optional pattern member: " +
                                        formalMemberRole.getName(), element, patternAnnotation);
                    }
                }
            }
        }
    }

}
